MCP (Model Context Protocol) crossed from experimental to essential in 2026. According to Stacklok's 2026 software report, 41% of surveyed engineering organizations are running MCP servers in limited or broad production. Official servers now ship from Google, Microsoft, Stripe, and Vercel. It's screened in mid/senior AI engineer interviews. If you build agent systems and haven't shipped an MCP server yet, this is the tutorial.

1. What MCP Actually Does

MCP solves the integration combinatorics problem. Without it, every AI agent needs a custom integration for every tool it might use — N agents times M tools equals a maintenance nightmare. MCP is the USB-C of AI: you write the server once, expose your tools via a standard protocol, and any MCP-compatible client (Claude Desktop, Claude Code, your own agent) can pick them up.

Tools
Functions the model can call. You define the schema; the model decides when to call.
Resources
Read-only data sources (files, DB records) the model can inspect.
Prompts
Reusable prompt templates the client can inject at conversation start.

For most practical agent work, you'll only use Tools. That's what this tutorial covers.

2. Project Setup

Node.js 22+ required (we use the built-in node:sqlite module — no native compilation needed). Initialize with ESM from the start; MCP SDK is ESM-only.

Shell mkdir my-mcp-server && cd my-mcp-server npm init -y npm install @modelcontextprotocol/sdk

Edit package.json to add "type": "module". This is the step that trips up most people — without it, the ESM imports in the MCP SDK will fail.

package.json { "name": "my-mcp-server", "type": "module", "scripts": { "start": "node src/server.js" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0" } }

3. The Minimal Server

An MCP server has three parts: the McpServer instance (registers tools), the transport (how clients connect), and the handler loop. Here's the minimal working version:

src/server.js import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const server = new McpServer({ name: "my-mcp-server", version: "1.0.0" }); server.tool( "add", "Add two numbers together.", { a: z.number(), b: z.number() }, async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }] }) ); const transport = new StdioServerTransport(); await server.connect(transport);
stdio vs HTTP stdio is the right default for local tools — it's zero-config, process-isolated, and works with Claude Desktop out of the box. HTTP transport is for remote/shared servers and has more production complexity (session management, load balancer compatibility). Start with stdio.

4. Adding Persistent State with SQLite

The real power comes when your tools maintain state between calls. Node.js 22's built-in node:sqlite gives you a synchronous, zero-dependency SQLite — no native compilation, no extra packages.

src/db.js import { DatabaseSync } from "node:sqlite"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); export function openDb(dbPath = join(__dirname, "..", "data", "store.db")) { const db = new DatabaseSync(dbPath); db.exec(` CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT UNIQUE NOT NULL, body TEXT NOT NULL, ts INTEGER DEFAULT (unixepoch()) ); `); return db; }
src/server.js — with persistence import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { openDb } from "./db.js"; const server = new McpServer({ name: "notes-server", version: "1.0.0" }); const db = openDb(); server.tool( "note_write", "Save a note under a key. Overwrites if key exists.", { key: z.string(), body: z.string() }, async ({ key, body }) => { db.prepare( "INSERT INTO notes (key, body) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET body=excluded.body" ).run(key, body); return { content: [{ type: "text", text: `Saved: ${key}` }] }; } ); server.tool( "note_read", "Read a note by key.", { key: z.string() }, async ({ key }) => { const row = db.prepare("SELECT body FROM notes WHERE key=?").get(key); return { content: [{ type: "text", text: row ? row.body : "Not found." }] }; } ); const transport = new StdioServerTransport(); await server.connect(transport);

5. Wire It Into Claude Desktop

Find your Claude Desktop config file:

claude_desktop_config.json { "mcpServers": { "notes-server": { "command": "node", "args": ["/absolute/path/to/my-mcp-server/src/server.js"] } } }

Restart Claude Desktop. Your tools will appear in the tool picker. If they don't show, run npx @modelcontextprotocol/inspector node src/server.js to test and debug your server interactively before touching the config again.

6. Tool Description Quality Matters More Than You Think

The model selects tools entirely based on your description strings. A vague description like "Write something" leads to incorrect tool selection. Be specific about what the tool does, what the parameters mean, and when to use it vs. alternatives.

Bad: "Write a note"
Good: "Save a note under a unique key. Use this to persist information across conversation turns. Overwrites any existing note at that key. Use note_read to retrieve it later."

7. Production Considerations

For local developer tools, stdio is production-ready as-is. For shared or remote servers, the 2026 MCP roadmap addresses the main gaps:

What I Built

My MCP Agent Toolkit extends this pattern to expose three production agent-kernel patterns as MCP tools: shared blackboard state (agents communicate without coupling), SCAR failure memory (fix the same error once, never twice), and LLM response cache (SHA-256 keyed, stops you paying for identical requests). 13 tests, all passing. Plug it into any Claude or GPT agent in under five minutes.